﻿using System.Collections.Generic;
using System.ComponentModel;
using Common.Utility;
using DataInterpolator.Model;
using DataInterpolator.ViewModel.Messages;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Messaging;
using System.Linq;
using System;
using OxyPlot;
using OxyPlot.Series;
using Common.ViewModel;
using System.Diagnostics;
using OxyPlot.Annotations;

namespace DataInterpolator.ViewModel
{
   /// <summary>
   /// This class contains properties that a View can data bind to.
   /// <para>
   /// See http://www.galasoft.ch/mvvm
   /// </para>
   /// </summary>
   public class IsoPlotsViewModel : ViewModelBase
   {
      private Dictionary<Guid, LineSeries> _TitleToTimeDepth = new Dictionary<Guid, LineSeries>();
      private Dictionary<Guid, ScatterSeries> _TitleToTimePach = new Dictionary<Guid, ScatterSeries>();
      private Dictionary<Guid, ScatterSeries> _TitleToTimeVel = new Dictionary<Guid, ScatterSeries>();
      private Dictionary<Guid, ScatterSeries> _TitleToPachVel = new Dictionary<Guid, ScatterSeries>();

      private readonly IDataService _DataService = null;

      /// <summary>
      /// Initializes a new instance of the IsoPlotsViewModel class.
      /// </summary>
      public IsoPlotsViewModel(IDataService dataService)
      {
         Stats = new IsoPlotStatsVMO();

         _DataService = dataService;

         Messenger.Default.Register<FullRefreshMessage>(this, OnFullRefresh);
         Messenger.Default.Register<PartialUpdateMessage>(this, OnPartialUpdate);

         SetUpPlotViewModels();

         Messenger.Default.Register<MenuCommandMessage>(this, OnMenuCommand);

         //Request any existing data
         Messenger.Default.Send(new RequestDataMessage(OnFullRefresh));
      }

      private void ResetAxes(PlotModel model)
      {
         foreach (var axis in model.Axes)
         {
            axis.Reset();
         }

         model.InvalidatePlot(true);
      }

      private void OnMenuCommand(MenuCommandMessage command)
      {
         if (command.CommandType == MenuCommandTypes.RefreshPlot)
         {
            ResetAxes(TimeDepthViewModel);
            ResetAxes(TimePachViewModel);
            ResetAxes(TimeVelViewModel);
            ResetAxes(PachVelViewModel);
         }
      }

      public PlotModel TimeDepthViewModel
      {
         get;
         private set;
      }

      public PlotModel TimePachViewModel
      {
         get;
         private set;
      }

      public PlotModel TimeVelViewModel
      {
         get;
         private set;
      }

      public PlotModel PachVelViewModel
      {
         get;
         private set;
      }

      public IsoPlotStatsVMO Stats
      {
         get; private set;
      }

      private void SetUpPlotViewModels()
      {
         TimeDepthViewModel = new PlotModel() { Title = "Time vs. Depth" };

         var xAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Bottom,
            Key = "xAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "Time",
            Unit = "ms",
            MinimumPadding = 0.05,
            MaximumPadding = 0.05
         };

         var yAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Left,
            StartPosition = 1,
            EndPosition = 0,
            Key = "yAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "Depth",
            Unit = "ft",
            MinimumPadding = 0.05,
            MaximumPadding = 0.05
         };

         TimeDepthViewModel.Axes.Add(xAxis);
         TimeDepthViewModel.Axes.Add(yAxis);

         TimeDepthViewModel.IsLegendVisible = true;
         TimeDepthViewModel.LegendPlacement = LegendPlacement.Outside;

         TimeDepthViewModel.InvalidatePlot(true);

         TimePachViewModel = new PlotModel();

         xAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Bottom,
            Key = "xAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "IsoTime",
            MinimumPadding = 0.25,
            MaximumPadding = 0.25
         };

         yAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Left,
            Key = "yAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "IsoPach",
            MinimumPadding = 0.25,
            MaximumPadding = 0.25
         };

         TimePachViewModel.Axes.Add(xAxis);
         TimePachViewModel.Axes.Add(yAxis);

         TimePachViewModel.IsLegendVisible = true;
         TimePachViewModel.LegendPlacement = LegendPlacement.Outside;

         TimePachViewModel.InvalidatePlot(true);

         TimeVelViewModel = new PlotModel();

         xAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Bottom,
            Key = "xAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "IsoTime",
            MinimumPadding = 0.25,
            MaximumPadding = 0.25
         };

         yAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Left,
            Key = "yAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "Interval Velocity",
            Unit = "ft/s",
            MinimumPadding = 0.25,
            MaximumPadding = 0.25
         };

         TimeVelViewModel.Axes.Add(xAxis);
         TimeVelViewModel.Axes.Add(yAxis);

         TimeVelViewModel.IsLegendVisible = true;
         TimeVelViewModel.LegendPlacement = LegendPlacement.Outside;

         TimeVelViewModel.InvalidatePlot(true);

         PachVelViewModel = new PlotModel();

         xAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Bottom,
            Key = "xAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "IsoPach",
            MinimumPadding = 0.25,
            MaximumPadding = 0.25
         };

         yAxis = new OxyPlot.Axes.LinearAxis()
         {
            Position = OxyPlot.Axes.AxisPosition.Left,
            Key = "yAxis",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.None,
            Title = "Interval Velocity",
            Unit = "ft/s",
            MinimumPadding = 0.25,
            MaximumPadding = 0.25
         };

         PachVelViewModel.Axes.Add(xAxis);
         PachVelViewModel.Axes.Add(yAxis);

         PachVelViewModel.IsLegendVisible = true;
         PachVelViewModel.LegendPlacement = LegendPlacement.Outside;

         PachVelViewModel.InvalidatePlot(true);
      }

      private void SetTitles(string topTitle, string botTitle)
      {
         TimePachViewModel.Title = topTitle + " - " + botTitle;
         TimeVelViewModel.Title = topTitle + " - " + botTitle;
         PachVelViewModel.Title = topTitle + " - " + botTitle;

         TimePachViewModel.InvalidatePlot(true);
         TimeVelViewModel.InvalidatePlot(true);
         PachVelViewModel.InvalidatePlot(true);
      }

      private MultiSelectBindingList<WellVMO> _InternalWellData = null;

      public MultiSelectBindingList<WellVMO> InternalWellData
      {
         get
         {
            return _InternalWellData;
         }
         set
         {
            //Need to move the event handlers
            if (_InternalWellData != null)
            {
               _InternalWellData.ListChanged -= OnItemChanged;
            }

            _InternalWellData = value;

            if (_InternalWellData != null)
            {
               _InternalWellData.ListChanged += OnItemChanged;
            }

            RaisePropertyChanged("InternalWellData");
         }
      }

      /// <summary>
      /// The <see cref="FormationOptions" /> property's name.
      /// </summary>
      public const string FormationOptionsPropertyName = "FormationOptions";

      private BindingList<string> _FormationOptions = new BindingList<string>();

      /// <summary>
      /// Sets and gets the FormationOptions property.
      /// Changes to that property's value raise the PropertyChanged event. 
      /// </summary>
      public BindingList<string> FormationOptions
      {
         get
         {
            return _FormationOptions;
         }

         set
         {
            if (_FormationOptions == value)
            {
               return;
            }

            _FormationOptions = value;
            RaisePropertyChanged(FormationOptionsPropertyName);
         }
      }

      /// <summary>
      /// The <see cref="TopFormationChoice" /> property's name.
      /// </summary>
      public const string TopFormationChoicePropertyName = "TopFormationChoice";

      private string _TopFormationChoice = "";

      /// <summary>
      /// Sets and gets the TopFormationChoice property.
      /// Changes to that property's value raise the PropertyChanged event. 
      /// </summary>
      public string TopFormationChoice
      {
         get
         {
            return _TopFormationChoice;
         }

         set
         {
            if (_TopFormationChoice == value)
            {
               return;
            }

            _TopFormationChoice = value;
            RaisePropertyChanged(TopFormationChoicePropertyName);

            SetTitles(TopFormationChoice, BotFormationChoice);

            _DataService.SetTopFormation(_TopFormationChoice);
         }
      }

      /// <summary>
          /// The <see cref="BotFormationChoice" /> property's name.
          /// </summary>
      public const string BotFormationChoicePropertyName = "BotFormationChoice";

      private string _BotFormationChoice = "";

      /// <summary>
      /// Sets and gets the BotFormationChoice property.
      /// Changes to that property's value raise the PropertyChanged event. 
      /// </summary>
      public string BotFormationChoice
      {
         get
         {
            return _BotFormationChoice;
         }

         set
         {
            if (_BotFormationChoice == value)
            {
               return;
            }

            _BotFormationChoice = value;
            RaisePropertyChanged(BotFormationChoicePropertyName);

            SetTitles(TopFormationChoice, BotFormationChoice);

            _DataService.SetBotFormation(_BotFormationChoice);
         }
      }


      private void OnFullRefresh(FullRefreshMessage message)
      {
         InternalWellData = message.DataView.SourceCollection as MultiSelectBindingList<WellVMO>;

         CreateAllSeries();
         SyncAllProps();
         SyncAllData();

         RefreshFormationOptions();
      }

      private void RefreshFormationOptions()
      {
         List<string> allOptions = new List<string>();

         foreach (var well in _InternalWellData)
         {
            if (!well.Visible)
               continue;

            foreach (var formation in well.TopsData.Formations)
            {
               if (!allOptions.Contains(formation.Name))
               {
                  allOptions.Add(formation.Name);
               }
            }
         }

         List<string> finalOptions = new List<string>();

         for (int i = 0; i < allOptions.Count; i++)
         {
            bool isMissing = false;

            foreach (var well in _InternalWellData)
            {
               if (!well.Visible)
                  continue;

               if (!well.TopsData.Contains(allOptions[i]))
               {
                  isMissing = true;
                  break;
               }
            }

            if (!isMissing)
            {
               finalOptions.Add(allOptions[i]);
            }
         }

         FormationOptions = new BindingList<string>(finalOptions);

         //save the old options
         string oldTop = TopFormationChoice;
         string oldBot = BotFormationChoice;

         int topIndex = Math.Max(FormationOptions.IndexOf(TopFormationChoice), 0);

         int botIndex = Math.Max(FormationOptions.IndexOf(BotFormationChoice), topIndex + 1);

         if (FormationOptions.Count >= topIndex + 1)
         {
            TopFormationChoice = FormationOptions[topIndex];
         }
         else
         {
            TopFormationChoice = "";
         }

         if (FormationOptions.Count >= botIndex + 1)
         {
            BotFormationChoice = FormationOptions[botIndex];
         }
         else
         {
            BotFormationChoice = "";
         }
      }

      private void OnItemChanged(object sender, ListChangedEventArgs e)
      {
         if (e.PropertyDescriptor.Name == WellBase.VisiblePropertyName)
         {
            SyncAllProps();
            SyncTrendLines();
            RefreshFormationOptions();
         }
         else if (e.PropertyDescriptor.Name == WellBase.FocusPropertyName)
         {
            SyncAllProps();
         }
         else if (e.PropertyDescriptor.Name == WellBase.TitlePropertyName)
         {
            CreateAllSeries();
            SyncAllProps();
            SyncAllData();

            RefreshFormationOptions();
         }
      }

      private void CreateAllSeries()
      {
         //Clear out all of the series and create new ones for each Well
         TimeDepthViewModel.Series.Clear();
         _TitleToTimeDepth.Clear();
         TimePachViewModel.Series.Clear();
         _TitleToTimePach.Clear();
         TimeVelViewModel.Series.Clear();
         _TitleToTimeVel.Clear();
         PachVelViewModel.Series.Clear();
         _TitleToPachVel.Clear();

         for (int i = 0; i < InternalWellData.Count; i++)
         {
            Series series = new LineSeries() { MarkerType = DefaultPalettes.Marker[i % DefaultPalettes.Marker.Length] };
            _TitleToTimeDepth[InternalWellData[i].ID] = (LineSeries)series;
            TimeDepthViewModel.Series.Add(series);

            series = new ScatterSeries() { MarkerType = DefaultPalettes.Marker[i % DefaultPalettes.Marker.Length] };
            _TitleToTimePach[InternalWellData[i].ID] = (ScatterSeries)series;
            TimePachViewModel.Series.Add(series);

            series = new ScatterSeries() { MarkerType = DefaultPalettes.Marker[i % DefaultPalettes.Marker.Length] };
            _TitleToTimeVel[InternalWellData[i].ID] = (ScatterSeries)series;
            TimeVelViewModel.Series.Add(series);

            series = new ScatterSeries() { MarkerType = DefaultPalettes.Marker[i % DefaultPalettes.Marker.Length] };
            _TitleToPachVel[InternalWellData[i].ID] = (ScatterSeries)series;
            PachVelViewModel.Series.Add(series);
         }
      }

      private void SyncAllProps()
      {
         for (int i = 0; i < InternalWellData.Count; i++)
         {
            SyncWellPropsToSeries(InternalWellData[i], _TitleToTimeDepth[InternalWellData[i].ID]);
            SyncWellPropsToSeries(InternalWellData[i], _TitleToTimePach[InternalWellData[i].ID]);
            SyncWellPropsToSeries(InternalWellData[i], _TitleToTimeVel[InternalWellData[i].ID]);
            SyncWellPropsToSeries(InternalWellData[i], _TitleToPachVel[InternalWellData[i].ID]);
         }

         TimeDepthViewModel.InvalidatePlot(true);
         TimePachViewModel.InvalidatePlot(true);
         TimeVelViewModel.InvalidatePlot(true);
         PachVelViewModel.InvalidatePlot(true);
      }

      private void SyncWellPropsToSeries(WellVMO well, Series series)
      {
         series.Title = well.Title;
         series.IsVisible = well.Visible;

         OxyColor color = OxyColor.FromArgb(well.Color.A, well.Color.R, well.Color.G, well.Color.B);

         switch (well.Focus)
         {
            case FocusLevels.Low:
               SeriesSetColorSize(series, OxyColor.FromAColor((byte)(well.Color.A / 10), color), 2.0);
               break;
            case FocusLevels.Normal:
               SeriesSetColorSize(series, color, 2.0);
               break;
            case FocusLevels.High:
               SeriesSetColorSize(series, color, 4.0);
               break;
            default:
               break;
         }
      }

      void SeriesSetColorSize(Series series, OxyColor color, double size)
      {
         LineSeries line = series as LineSeries;
         if (line != null)
         {
            line.Color = color;
            line.StrokeThickness = size;
         }

         ScatterSeries scatter = series as ScatterSeries;

         if (scatter != null)
         {
            scatter.MarkerFill = color;
            scatter.MarkerStroke = color;

            scatter.MarkerSize = 1.0 + size;
            scatter.MarkerStrokeThickness = 1.0;
         }
      }

      private void SyncAllData()
      {
         for (int i = 0; i < InternalWellData.Count; i++)
         {
            Guid id = InternalWellData[i].ID;

            SyncWellDataToSeries(InternalWellData[i], _TitleToTimeDepth[id], _TitleToTimePach[id], _TitleToTimeVel[id], _TitleToPachVel[id]);
         }

         SyncTrendLines();

         //This VM doesnt get updated with the trend lines
         TimeDepthViewModel.InvalidatePlot(true);
      }

      private void SyncTrendLines()
      {
         AddRegressionLine(TimePachViewModel, _TitleToTimePach.Values.ToList());
         AddRegressionLine(TimeVelViewModel, _TitleToTimeVel.Values.ToList());
         AddRegressionLine(PachVelViewModel, _TitleToPachVel.Values.ToList());

         Stats.Update(InternalWellData);

         TimePachViewModel.InvalidatePlot(true);
         TimeVelViewModel.InvalidatePlot(true);
         PachVelViewModel.InvalidatePlot(true);
      }

      private void SyncWellDataToSeries(WellVMO well, LineSeries timeDepth, ScatterSeries timePach, ScatterSeries timeVel, ScatterSeries pachVel)
      {
         timeDepth.Points.Clear();
         timePach.Points.Clear();
         timeVel.Points.Clear();
         pachVel.Points.Clear();

         timeDepth.Points.AddRange(well.OutputData.Data);

         timePach.Points.Add(new ScatterPoint(well.IsoData.IsoTime, well.IsoData.IsoPach));
         timeVel.Points.Add(new ScatterPoint(well.IsoData.IsoTime, well.IsoData.IntervalVelocity));
         pachVel.Points.Add(new ScatterPoint(well.IsoData.IsoPach, well.IsoData.IntervalVelocity));
      }

      private void AddRegressionLine(PlotModel model, List<ScatterSeries> series)
      {
         List<double> xVals = new List<double>();
         List<double> yVals = new List<double>();

         foreach (var s in series)
         {
            if (!s.IsVisible)
               continue;

            foreach (var p in s.Points)
            {
               xVals.Add(p.X);
               yVals.Add(p.Y);
            }
         }

         double rSquared = 1.0;
         double yIntercept = 0.0;
         double slope = 0.0;

         LinearRegression(xVals.ToArray(), yVals.ToArray(), 0, xVals.Count, out rSquared, out yIntercept, out slope);

         for (int i = 0; i < model.Series.Count; i++)
         {
            if (model.Series[i] is FunctionSeries)
            {
               model.Series.RemoveAt(i);
               break;
            }
         }

         model.Annotations.Clear();

         var annotation = new LineAnnotation();
         annotation.Intercept = yIntercept;
         annotation.Slope = slope;
         annotation.Text = "y = " + slope.ToString("G7") + "x + " + yIntercept.ToString("G7") + Environment.NewLine + "r^2 = " + rSquared.ToString("G7");
         annotation.Type = LineAnnotationType.LinearEquation;

         model.Annotations.Add(annotation);
      }



      public static void LinearRegression(double[] xVals, double[] yVals,
                                        int inclusiveStart, int exclusiveEnd,
                                        out double rsquared, out double yintercept,
                                        out double slope)
      {
         Debug.Assert(xVals.Length == yVals.Length);
         double sumOfX = 0;
         double sumOfY = 0;
         double sumOfXSq = 0;
         double sumOfYSq = 0;
         double ssX = 0;
         double ssY = 0;
         double sumCodeviates = 0;
         double sCo = 0;
         double count = exclusiveEnd - inclusiveStart;

         for (int ctr = inclusiveStart; ctr < exclusiveEnd; ctr++)
         {
            double x = xVals[ctr];
            double y = yVals[ctr];
            sumCodeviates += x * y;
            sumOfX += x;
            sumOfY += y;
            sumOfXSq += x * x;
            sumOfYSq += y * y;
         }
         ssX = sumOfXSq - ((sumOfX * sumOfX) / count);
         ssY = sumOfYSq - ((sumOfY * sumOfY) / count);
         double RNumerator = (count * sumCodeviates) - (sumOfX * sumOfY);
         double RDenom = (count * sumOfXSq - (sumOfX * sumOfX))
          * (count * sumOfYSq - (sumOfY * sumOfY));
         sCo = sumCodeviates - ((sumOfX * sumOfY) / count);

         double meanX = sumOfX / count;
         double meanY = sumOfY / count;
         double dblR = RNumerator / Math.Sqrt(RDenom);
         rsquared = dblR * dblR;
         yintercept = meanY - ((sCo / ssX) * meanX);
         slope = sCo / ssX;
      }

      private void OnPartialUpdate(PartialUpdateMessage message)
      {
         if ((message.UpdateType & (PartialUpdateTypes.Output | PartialUpdateTypes.Analysis)) > 0)
         {
            SyncAllData();
         }
      }
   }
}